package org.dandsoft.blackbox.snn;

import org.dandsoft.blackbox.snn.study.Teacher;

import java.util.*;

/**
 * @author Vilyuzhanin Denis
 */
public class Environment {

    private boolean isTimeFeeling;
    private Set<ObjectId> inputs = new HashSet<ObjectId>();
    private Set<ObjectId> outputs = new HashSet<ObjectId>();
    private Map<ObjectId, Signal> enviroment = new Hashtable<ObjectId, Signal>();
    private Map<ObjectId, Signal> newEnviroment = new Hashtable<ObjectId, Signal>();
    private Map<ObjectId, Value> backSignal = new Hashtable<ObjectId, Value>();
    private LinkedList<Signal> anomalies = new LinkedList<Signal>();
    private HashSet<Signal> useless = new HashSet<Signal>();
    private HashSet<Signal> newUseless = new HashSet<Signal>();
    private int sequence;
    private EnviromentTraceWriter traceWriter;
    private Container containter = new Container();
    private EnviromentMonitor monitor = new NothingEnviromentMonitor();
    private Time time = new Time(0);
    private Teacher teacher = new Teacher();


    /**
     * <p>
     * Создать входной сигнал. Не создает триггеров для обслуживания сигнала.
     * </p>
     *
     * @return Идентификатор входного сигнала
     */
    public ObjectId createInput() {
        ObjectId id = createObjectId();
        inputs.add(id);
        return id;
    }

    /**
     * <p>
     * Удалить входной сигнал. Удаляет его из списка входных сигналов, но не удаляет синапсы которые
     * слушают этот сигнал. Если сигнал удалить, то синапсы должны деградировать, при длительной пассивности
     * сигнала.
     * </p>
     *
     * @param id Идентификатор сигнала
     */
    public void removeInput(ObjectId id) {
        inputs.remove(id);
    }

    /**
     * <p>
     * Подать на вход сигнал. Сигнал должен быть создан и должен находится в списке входных сигналов.
     * </p>
     *
     * @param id    Идентификатор сигнала
     * @param value Активность сигнала.
     */
    public void input(ObjectId id, Value value) {
        if (inputs.contains(id)) {
            Signal signal = doPut(id, value);
            monitor.logInput(time, signal);
        }
    }

    /**
     * <p>
     * Создать выходной сигнал. Создает триггер, c типом
     * {@link org.dandsoft.blackbox.snn.Trigger.State#Output Output}
     * </p>
     *
     * @return Идентификатор сигнала
     */
    public ObjectId createOutput() {
        ObjectId id = createObjectId();
        Trigger newTrigger = new Trigger();
        newTrigger.setId(id);
        containter.addTrigger(newTrigger);
        outputs.add(id);
        return id;
    }

    /**
     * <p>
     * Получить значение выходного сигнала. Сигнал должен находится в списке выходных сигналов
     * </p>
     *
     * @param id Идентификатор сигнала
     * @return Значение автивности сигнала или  null если активности нет, или сигнал не найден в списке активных
     */
    public Value output(ObjectId id) {
        if (outputs.contains(id)) {
            Signal signal = enviroment.get(id);
            if (signal != null) {
                return signal.getValue();
            } else {
                return null;
            }
        } else {
            return null;
        }
    }

    /**
     * <p>
     * Удалить выходной сигнал. Удаляет сигнал из списка выходных, но не удаляет триггер генерирующий сигнал.
     * Триггер должен превратиться в самый обыкновенный триггер. И если этого потребует обучение может быть удален
     * </p>
     *
     * @param id
     */
    public void removeOutput(ObjectId id) {
        outputs.remove(id);
    }

    public void setSequence(int seq) {
        this.sequence = seq;
    }

    public synchronized ObjectId createObjectId() {
        return new ObjectId(sequence++);
    }

    private Signal doPut(ObjectId id, Value value) {
        if (value.isGreater(Value.PASSIVE)) {
            Signal signal = new Signal(id, value);
            newEnviroment.put(id, signal);
            newUseless.add(signal);
            return signal;
        } else {
            return null;
        }
    }

    public void put(ObjectId id, Value value) {
        Signal signal = doPut(id, value);
        monitor.logNewSigal(time, signal);
        if (outputs.contains(id)) {
            monitor.logOutput(time, signal);
        }
    }

    public Value get(ObjectId id) {
        Signal signal = enviroment.get(id);
        if (signal != null) {
            signal.setUsed(true);
            return signal.getValue();
        } else {
            return null;
        }
    }

    public void notifyUsefulUsing(ObjectId triggerId, ObjectId signalId) {
        Signal signal = enviroment.get(signalId);
        useless.remove(signal);

        //[LOG]
        monitor.logUsefulUsing(time, signal);
    }

    public void putAnomaly(ObjectId triggerId, ObjectId signalId, Time time) {
        Signal signal = enviroment.get(signalId);
        if (signal != null) {
            signal.setAnomaly(true);
            signal.setTime(time);
            anomalies.add(signal);

            //[LOG]
            monitor.logAnomaly(time, signal);
        }
    }

    /**
     * <p>
     * Выполняем шаг моделирования
     * </p>
     */
    public void alive() {
        Map<ObjectId, Signal> temp = enviroment;
        HashSet<Signal> tempSet = useless;
        enviroment = newEnviroment;
        useless = newUseless;
        newEnviroment = temp;
        newUseless = tempSet;
        newEnviroment.clear();
        newUseless.clear();
        anomalies.clear();
        this.time = new Time(this.time.getValue() + 1);
        containter.alive(this);
        //[LOG]
        monitor.logEnviroment(time, enviroment);

    }


    /**
     * <p>
     * Выполняет сброс состояния окружения. Все данные о текущей активности, прошлой активности, бесполезных
     * сигналах и аномалиях отчищаются.
     * </p>
     */
    public void reset() {
        newEnviroment.clear();
        enviroment.clear();
        useless.clear();
        newUseless.clear();
        anomalies.clear();
    }

    public void study() {
        studyUseless();
        //containter.study(this, teacher);
    }

    protected void studyUseless() {
        //���� ���� ������ ���� ������, �� ��� ���� �� ��������� ����� �������
        //��� ��� �� ��� ���� ��������
        if (this.useless.size() > 1) {
            Trigger newTrigger = new Trigger();
            newTrigger.setState(Trigger.State.New);
            newTrigger.setId(createObjectId());
            for (Signal useless : this.useless) {
                newTrigger.connect(useless.getSignalId(), useless.getValue());
            }
            containter.addTrigger(newTrigger);

            //[LOG]
            monitor.logNewTrigger(time, newTrigger);
        }
    }


    public void printTrace() {
        for (Map.Entry<ObjectId, Signal> entry : enviroment.entrySet()) {
            traceWriter.put(entry.getKey(), entry.getValue().getValue());
        }
        traceWriter.flush(0);
    }

    public EnviromentTraceWriter getTrace() {
        return traceWriter;
    }

    public void setTrace(EnviromentTraceWriter traceWriter) {
        this.traceWriter = traceWriter;
    }


    public EnviromentMonitor getMonitor() {
        return monitor;
    }

    public void setMonitor(EnviromentMonitor monitor) {
        if (monitor == null) {
            this.monitor = new NothingEnviromentMonitor();
        } else {
            this.monitor = monitor;
        }
    }

    public Time getTime() {
        return time;
    }

    public void setTime(Time time) {
        this.time = time;
    }

    /**
     * <p>
     * Возвращает значение флага <i>"ощущение времени"</i>. Этот флаг выключает механизмы в системе, которые
     * позволяют анализировать образы во времени. Без него при анализе и обучении учитываются только сигналы
     * которые активны в данные момент веремени.
     * </p>
     *
     * @return false - система не ощущает времени. true - система ощущает время.
     */
    public boolean isTimeFeeling() {
        return isTimeFeeling;
    }

    /**
     * <p>
     * Установить флаг <i>"ощущение времени"</i>
     * </p>
     *
     * @param timeFeeling
     * @see Environment#isTimeFeeling()
     */
    public void setTimeFeeling(boolean timeFeeling) {
        isTimeFeeling = timeFeeling;
    }

    public Teacher getTeacher() {
        return teacher;
    }

    public void setTeacher(Teacher teacher) {
        this.teacher = teacher;
    }

    public HashSet<Signal> getUseless() {
        return useless;
    }
}
